package pl.mdziedzic.unittests;

import java.util.Calendar;
import java.util.TreeSet;

import org.apache.commons.lang.ArrayUtils;

import pl.mdziedzic.unittests.exception.InvalidObjectStatusException;
import pl.mdziedzic.unittests.exception.InvalidObjectTypeException;
import pl.mdziedzic.unittests.exception.InvalidUserException;
import pl.mdziedzic.unittests.exception.NotSufficientFoundsException;
import pl.mdziedzic.unittests.exception.OperationNotAllowedException;
import pl.mdziedzic.unittests.services.HISTORY_EVENT;
import pl.mdziedzic.unittests.services.MAIL_TEMPLATE;
import pl.mdziedzic.unittests.services.MailProvider;
import pl.mdziedzic.unittests.services.RequestContext;

import com.google.common.collect.Sets;

public class Auction {
	
	private RequestContext context;
	
	private long id;
	private String name;
	private Calendar startTime;
	private Calendar endTime;
	private Calendar activateDate;
	private Calendar cancelDate;
	private AUCTION_STATUS status;
	private AUCTION_TYPE type;
	private long startingPrice;
	private long buyNowPrice;
	private User owner;
	
	private TreeSet<Bid> bids = Sets.newTreeSet();	

	public Auction(RequestContext context, CreateAuctionRequest request) {
		this.context = context;
		
		mustBeFromFuture(request.getStartTime());
		mustBeFromFuture(request.getEndTime());
		
		if (request.getEndTime().before(request.getStartTime())) {
			throw new OperationNotAllowedException("End time must be after current time and start time");
    }
		
		this.name = request.getName();
		this.startTime = request.getStartTime();
		this.endTime = request.getEndTime();
		this.type = request.getType();
		this.startingPrice = request.getStartingPrice();
		this.buyNowPrice = request.getBuyNowPrice();
		
		this.owner = context.getUser();
		this.status = AUCTION_STATUS.NEW;
		
		context.getHistoryProvider().saveHistory(HISTORY_EVENT.AUCTION_CREATED);
	}
	
	public void activate() {
		mustBeStatus(AUCTION_STATUS.NEW);
		mustBeFromPast(startTime);
		
		status = AUCTION_STATUS.ACTIVE;
		activateDate = context.getNow();
		
		context.getHistoryProvider().saveHistory(HISTORY_EVENT.AUCTION_ACTIVATED);
	}
	
	public void bid(long price) {
		mustBeType(AUCTION_TYPE.BID);
		
		if (startingPrice > price) {
	    throw new NotSufficientFoundsException("Current bid price must be heigher than starting price");
    }
		
		if (hasBids()) {
			
			Bid lastBid = bids.last();
			if (lastBid != null && lastBid.getPrice() > price) {
		    throw new NotSufficientFoundsException("Current bid price must be heigher than last bid price");
	    }
    }
		
		addBid(price, HISTORY_EVENT.BID_ADDED);
	}
	
	public void buyNow() {
		mustBeType(AUCTION_TYPE.BUY_NOW);
		
		addBid(buyNowPrice, HISTORY_EVENT.BOUGHT_NOW);
		
		finish();
	}
	
	private void addBid(long price, HISTORY_EVENT historyEvent) {
		
		mustBeStatus(AUCTION_STATUS.ACTIVE);
		mustNotBeOwner();
		
		Bid bid = new Bid(price, context.getUser(), context.getNow());
		bids.add(bid);
		
		context.getHistoryProvider().saveHistory(historyEvent);
	}
	
	public void finish() {
		mustBeStatus(AUCTION_STATUS.ACTIVE);
		
		if (isType(AUCTION_TYPE.BID) && endTime.before(context.getNow())) {
	    throw new OperationNotAllowedException("Auction has to be after its finish time");
    }
		
		if (hasBids()) {
			notifyBuyer();
			
	    context.getHistoryProvider().saveHistory(HISTORY_EVENT.AUCTION_FINISHED);
    }
		
		notifySeller();
		
		status = AUCTION_STATUS.FINISHED;
	}
	
	private void notifyBuyer() {
		context.getMailProvider().sendMail(MAIL_TEMPLATE.YOU_BOUGHT_AUCTION);
	}
	
	private void notifySeller() {
		MailProvider mailProvider = context.getMailProvider();
		
		if (hasBids()) {
	    mailProvider.sendMail(MAIL_TEMPLATE.YOUR_AUCTION_IS_SOLD);
    } else {
    	mailProvider.sendMail(MAIL_TEMPLATE.YOUR_AUCTION_WAS_NOT_SOLD);
    }
	}
	
	public boolean isType(AUCTION_TYPE expectedType) {
		return type.equals(expectedType);
	}
	
	public boolean hasBids() {
		return bids.size() != 0;
	}
	
	private void mustBeFromFuture(Calendar time) {
		if (time.before(context.getNow())) {
			throw new OperationNotAllowedException("Given time must be from future");
    }
	}
	
	private void mustBeFromPast(Calendar time) {
		if (time.after(context.getNow())) {
			throw new OperationNotAllowedException("Given time must be from past");
    }
	}
	
	private void mustBeStatus(AUCTION_STATUS... exptectedStatus) {
		if (isStatusIn(exptectedStatus) == false) {
	    throw new InvalidObjectStatusException(String.format("Current auction status is %s, expected %s", status, exptectedStatus));
    }
	}
	
	private void mustNotBeStatus(AUCTION_STATUS... expectedStatus) {
		if (isStatusIn(expectedStatus)) {
			throw new InvalidObjectStatusException("Auction status can't be " + expectedStatus);
		}
	}
	
	private boolean isStatusIn(AUCTION_STATUS... exptectedStatus) {
		return ArrayUtils.contains(exptectedStatus, status);
	}
	
	private void mustBeType(AUCTION_TYPE expectedType) {
		if (!isType(expectedType)) {
	    throw new InvalidObjectTypeException(String.format("Current auction type is %s, expected %s", type, expectedType));
    }
	}
	
	private void mustNotBeOwner() {
		if (isCalledByOwner()) {
	    throw new InvalidUserException("Operation not allowed for auction owner");
    }
	}
	
	private void mustBeOwner() {
		if (isCalledByOwner() == false) {
	    throw new InvalidUserException("Operation not allowed for non auction owner");
    }
	}
	
	private boolean isCalledByOwner() {
		return owner.equals(context.getUser());
	}

	public long getId() {
  	return id;
  }

	public String getName() {
  	return name;
  }

	public Calendar getStartTime() {
  	return startTime;
  }

	public Calendar getEndTime() {
  	return endTime;
  }

	public Calendar getActivateDate() {
  	return activateDate;
  }
	
	public Calendar getCancelDate() {
		return cancelDate;
	}

	public AUCTION_STATUS getStatus() {
  	return status;
  }

	public AUCTION_TYPE getType() {
  	return type;
  }

	public long getStartingPrice() {
  	return startingPrice;
  }

	public long getBuyNowPrice() {
  	return buyNowPrice;
  }

	public User getOwner() {
  	return owner;
  }

	public TreeSet<Bid> getBids() {
  	return bids;
  }

	@Override
  public String toString() {
	  return "Auction [auctionName=" + name + ", status=" + status + ", type=" + type + "]";
  }

}
